﻿/* Adapted from CSS2XPath - http://code.google.com/p/css2xpath/
 * Uses the same logic, all that has changed is some of the DOM structure of the code to conform to Resharper and some 
 * Stylecop recommendations
 */

namespace Webinator.Toolboxes
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Text.RegularExpressions;

    public class Css2Xpath
    {
        private readonly List<Regex> _patterns;
        private readonly List<Object> _replacements;

        public Css2Xpath()
        {
            // Initalize list of patterns and replacements
            _patterns = new List<Regex>();
            _replacements = new List<Object>();

            // Generate all the rules

            // Attributes
            AddRule(new Regex(@"\[([^\]~\$\*\^\|\!]+)(=[^\]]+)?\]"), "[@$1$2]");

            // Multiple queries
            AddRule(new Regex(@"\s*,\s*"), "|");

            // Remove space around +, ~, and >
            AddRule(new Regex(@"\s*(\+|~|>)\s*"), "$1");

            //Handle *, ~, +, and >
            AddRule(new Regex(@"([a-zA-Z0-9_\-\*])~([a-zA-Z0-9_\-\*])"), "$1/following-sibling::$2");
            AddRule(new Regex(@"([a-zA-Z0-9_\-\*])\+([a-zA-Z0-9_\-\*])"), "$1/following-sibling::*[1]/self::$2");
            AddRule(new Regex(@"([a-zA-Z0-9_\-\*])>([a-zA-Z0-9_\-\*])"), "$1/$2");

            // Escaping
            AddRule(new Regex(@"\[([^=]+)=([^'|" + "\"" + @"][^\]]*)\]"), "[$1='$2']");

            // All descendant or self to //
            AddRule(new Regex(@"(^|[^a-zA-Z0-9_\-\*])(#|\.)([a-zA-Z0-9_\-]+)"), "$1*$2$3");
            AddRule(new Regex(@"([\>\+\|\~\,\s])([a-zA-Z\*]+)"), "$1//$2");
            AddRule(new Regex(@"\s+\/\/"), "//");

            // Handle :first-child
            AddRule(new Regex(@"([a-zA-Z0-9_\-\*]+):first-child"), "*[1]/self::$1");

            // Handle :last-child
            AddRule(new Regex(@"([a-zA-Z0-9_\-\*]+):last-child"), "$1[not(following-sibling::*)]");

            // Handle :only-child
            AddRule(new Regex(@"([a-zA-Z0-9_\-\*]+):only-child"), "*[last()=1]/self::$1");

            // Handle :empty
            AddRule(new Regex(@"([a-zA-Z0-9_\-\*]+):empty"), "$1[not(*) and not(normalize-space())]");

            // Handle :not
            AddRule(new Regex(@"([a-zA-Z0-9_\-\*]+):not\(([^\)]*)\)"), m => m.Groups[1].Value + "[not(" + (new Regex("^[^\\[]+\\[([^\\]]*)\\].*$")).Replace(Transform(m.Groups[2].Value), "$1") + ")]");

            // Handle :nth-child
            AddRule(new Regex(@"([a-zA-Z0-9_\-\*]+):nth-child\(([^\)]*)\)"), m =>
                                                                                 {
                                                                                     String b = m.Groups[2].Value;
                                                                                     String a = m.Groups[1].Value;

                                                                                     switch (b)
                                                                                     {
                                                                                         case "n":
                                                                                             return a;
                                                                                         case "even":
                                                                                             return "*[position() mod 2=0 and position()>=0]/self::" + a;
                                                                                         case "odd":
                                                                                             return a + "[(count(preceding-sibling::*) + 1) mod 2=1]";
                                                                                         default:
                                                                                             // Parse out the 'n'
                                                                                             b = ((new Regex("^([0-9])*n.*?([0-9])*$")).Replace(b, "$1+$2"));

                                                                                             // Explode on + (i.e 'nth-child(2n+0)' )
                                                                                             var b2 = new String[2];
                                                                                             String[] splitResult = b.Split('+');

                                                                                             // The first component will always be a number
                                                                                             b2[0] = splitResult[0];

                                                                                             int buffer = 0;

                                                                                             // The second component might be missing
                                                                                             if (splitResult.Length == 2)
                                                                                                 if (!int.TryParse(splitResult[1], out buffer))
                                                                                                     buffer = 0;

                                                                                             b2[1] = buffer.ToString(CultureInfo.InvariantCulture);

                                                                                             return "*[(position()-" + b2[1] + ") mod " + b2[0] + "=0 and position()>=" + b2[1] + "]/self::" + a;
                                                                                     }
                                                                                 });

            // Handle :contains
            AddRule(new Regex(@":contains\(([^\)]*)\)"), m => "[contains(string(.),'" + m.Groups[1].Value + "')]");

            // != attribute
            AddRule(new Regex(@"\[([a-zA-Z0-9_\-]+)\|=([^\]]+)\]"), "[@$1=$2 or starts-with(@$1,concat($2,'-'))]");

            // *= attribute
            AddRule(new Regex(@"\[([a-zA-Z0-9_\-]+)\*=([^\]]+)\]"), "[contains(@$1,$2)]");

            // ~= attribute
            AddRule(new Regex(@"\[([a-zA-Z0-9_\-]+)~=([^\]]+)\]"), "[contains(concat(' ',normalize-space(@$1),' '),concat(' ',$2,' '))]");

            // ^= attribute
            AddRule(new Regex(@"\[([a-zA-Z0-9_\-]+)\^=([^\]]+)\]"), "[starts-with(@$1,$2)]");

            // $= attribute
            AddRule(new Regex(@"\[([a-zA-Z0-9_\-]+)\$=([^\]]+)\]"), m =>
                                                                        {
                                                                            String a = m.Groups[1].Value;
                                                                            String b = m.Groups[2].Value;
                                                                            return "[substring(@" + a + ",string-length(@" + a + ")-" + (b.Length - 3) + ")=" + b + "]";
                                                                        });

            // != attribute
            AddRule(new Regex(@"\[([a-zA-Z0-9_\-]+)\!=([^\]]+)\]"), "[not(@$1) or @$1!=$2]");

            // ID and class
            AddRule(new Regex(@"#([a-zA-Z0-9_\-]+)"), "[@id='$1']");
            AddRule(new Regex(@"\.([a-zA-Z0-9_\-]+)"), "[contains(concat(' ',normalize-space(@class),' '),' $1 ')]");

            // Normalize filters
            AddRule(new Regex(@"\]\[([^\]]+)"), " and ($1)");
        }

        public void AddRule(Regex regex, MatchEvaluator replacement)
        {
            AddRegexRule(regex, replacement);
        }

        public void AddRule(Regex regex, String replacement)
        {
            AddRegexRule(regex, replacement);
        }

        private void AddRegexRule(Regex regex, Object replacement)
        {
            if (regex == null)
            {
                throw new ArgumentException("Must supply non-null Regex.", "regex");
            }

            if (replacement == null || (!(replacement is String) && !(replacement is MatchEvaluator)))
            {
                throw new ArgumentException("Must supply non-null replacement (either String or MatchEvaluator).", "replacement");
            }

            _patterns.Add(regex);
            _replacements.Add(replacement);
        }

        public String Transform(String css)
        {
            int len = _patterns.Count;

            for (int i = 0; i < len; i++)
            {
                Regex pattern = _patterns[i];
                Object replacement = _replacements[i];

                // Depending on what the replacement is, we need to cast it to either a String or a MatchEvaluator
                if (replacement is String)
                {
                    css = pattern.Replace(css, (String)replacement);
                }
                else
                {
                    css = pattern.Replace(css, (MatchEvaluator)replacement);
                }
            }

            return "//" + css;
        }

        public void PreloadRules() { }
    }
}
